package net.seratch.taskun.scheduler.crond;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import net.seratch.taskun.common.ComponentContainerAdaptor;
import net.seratch.taskun.scheduler.util.CrontabUtil;
import net.seratch.taskun.scheduler.util.SchedulerUtil;
import net.seratch.taskun.util.CalendarUtil;
import net.seratch.taskun.util.StringUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Cron daemon worker
 * 
 * @author Kazuhiro Sera
 */
public class CronDaemon implements Runnable {

	/**
	 * Default crontab text filename
	 */
	public static final String defaultCrontabFileName = "crontab.txt";

	/**
	 * Logger
	 */
	protected Log log = LogFactory.getLog(getClass());

	/**
	 * Cron daemon log output prefix
	 */
	protected static final String LOG_PREFIX = "[Crond] ";

	/**
	 * executorService
	 */
	private ScheduledExecutorService executorService;

	/**
	 * Component container adaptor
	 */
	private ComponentContainerAdaptor containerAdaptor;

	/**
	 * is already initialized or not
	 */
	private boolean isInitialized = false;

	/**
	 * Crontab lines already read
	 */
	private List<Crontab> crontabList = new ArrayList<Crontab>();

	/**
	 * Previous checked timestamp in milliseconds
	 */
	private long previousCheckedTime = 0L;

	/**
	 * This server's name (if this server has been named)
	 */
	private String thisServerName;

	/**
	 * This server's hostname
	 */
	private String thisServerHostname;

	@Override
	public void run() {
		loggingAtEachInvocation(LOG_PREFIX + "Previous : "
				+ new Date(previousCheckedTime).toString() + " ("
				+ previousCheckedTime + ")");
		if (!isInitialized) {
			throw new IllegalStateException("Need to initialize CronDaemon!");
		}
		Calendar current = CalendarUtil.getCurrentTime();
		loggingAtEachInvocation(LOG_PREFIX + "Current  : "
				+ current.getTime().toString() + " ("
				+ current.getTimeInMillis() + ")");
		long currentCheckTime = current.getTimeInMillis();
		for (Crontab crontab : crontabList) {
			if (crontab.isIntervalInvocation) {
				continue;
			}
			// check the host
			if (crontab.namedServers != null) {
				boolean isHostToWork = false;
				for (String name : crontab.namedServers) {
					if (thisServerName.equals(name)) {
						isHostToWork = true;
						break;
					}
				}
				if (!isHostToWork) {
					continue;
				}
			}
			loggingAtEachInvocation(LOG_PREFIX + "Scheduled : " + crontab.line
					+ " (Next:"
					+ new Date(crontab.nextInvocationTime).toString() + ")");
			// check invocation timestamp
			if (crontab.nextInvocationTime > previousCheckedTime
					&& crontab.nextInvocationTime <= currentCheckTime) {
				try {
					Runnable instance = getCommand(crontab);
					long multiplicity = crontab.multiplicity;
					long initialDelay = crontab.initialIntervalSeconds * 1000;
					for (int i = 0; i < multiplicity; i++) {
						initialDelay += 100L; // distribute threads
						executorService.schedule(instance, initialDelay,
								TimeUnit.MILLISECONDS);
						loggingAtEachInvocation(LOG_PREFIX + ">> Invoked : "
								+ crontab.commandClassName);
					}
					crontab.nextInvocationTime = CrontabUtil
							.getNextInvocationTime(current, crontab);
					loggingAtEachInvocation(LOG_PREFIX + ">> Renewed : "
							+ crontab.line + "(Next:"
							+ new Date(crontab.nextInvocationTime).toString()
							+ ")");
				} catch (Exception e) {
					log.error(this.getClass().getCanonicalName()
							+ " failed to execute scheduled task : "
							+ crontab.commandClassName);
					e.printStackTrace();
				}
			}
		}
		previousCheckedTime = currentCheckTime;
	}

	/**
	 * Get Runnnable class to invoke as command
	 * 
	 * @param crontab
	 *            crontab object
	 * @return Runnable command class
	 * @throws Exception
	 */
	private Runnable getCommand(Crontab crontab) throws Exception {
		Class<?> clazz = Class.forName(crontab.commandClassName);
		Runnable instance = null;
		try {
			instance = containerAdaptor.getComponent(clazz);
		} catch (Exception e) {
			log.debug("Command class load failed! class name : "
					+ clazz.getCanonicalName());
		}
		if (instance == null) {
			instance = (Runnable) clazz.newInstance();
		}
		return instance;
	}

	/**
	 * Initialize cron daemon
	 * 
	 * @param container
	 *            component container adaptor
	 * @param executorService
	 *            executorService
	 */
	public void initialize(ComponentContainerAdaptor container,
			ScheduledExecutorService executorService) {
		initialize(container, executorService, defaultCrontabFileName);
	}

	/**
	 * Initialize cron daemon
	 * 
	 * @param container
	 *            component container adaptor
	 * @param executorService
	 *            executorService
	 * @param crontabFile
	 *            specified crontab file path
	 */
	public void initialize(ComponentContainerAdaptor containerAdaptor,
			ScheduledExecutorService executorService, String crontabFile) {
		this.containerAdaptor = containerAdaptor;
		this.executorService = executorService;
		InputStream is = null;
		BufferedReader br = null;
		try {
			// read crontab.txt
			is = this.getClass().getClassLoader().getResourceAsStream(
					crontabFile);
			if (is == null) {
				log.info("Skipped the crontab scheduing"
						+ " because crontab.txt did not found.");
				return;
			}
			br = new BufferedReader(new InputStreamReader(is));
			String line = null;
			while ((line = br.readLine()) != null) {
				addCrontab(line);
			}
			// working server config
			thisServerHostname = SchedulerUtil.getHostname();
			thisServerName = SchedulerUtil.getWorkingServerName(
					containerAdaptor.getSchedulerConfig(), thisServerHostname);
			if (StringUtil.isEmpty(thisServerName)) {
				thisServerName = thisServerHostname;
			}
			// start interval invocations
			List<Crontab> newList = new ArrayList<Crontab>();
			log.info("----- Taskun-scheduler initialized -----");
			log.info("Working at " + thisServerName + "(" + thisServerHostname
					+ ")");
			for (Crontab crontab : crontabList) {
				if (crontab.isIntervalInvocation) {
					try {
						// check the host
						if (crontab.namedServers != null) {
							boolean isHostToWork = false;
							for (String name : crontab.namedServers) {
								if (thisServerName.equals(name)) {
									isHostToWork = true;
									break;
								}
							}
							if (!isHostToWork) {
								continue;
							}
						}
						// invoke command
						Runnable command = getCommand(crontab);
						long multiplicity = crontab.multiplicity;
						long initialDelay = crontab.initialIntervalSeconds * 1000;
						long delay = crontab.intervalSeconds * 1000;
						for (int i = 0; i < multiplicity; i++) {
							initialDelay += 100L; // distribute threads
							executorService.scheduleAtFixedRate(command,
									initialDelay, delay, TimeUnit.MILLISECONDS);
						}
						log.info("Interval invocation : "
								+ crontab.intervalSeconds + "sec,"
								+ crontab.commandClassName + ","
								+ crontab.multiplicity);
					} catch (Exception e) {
						log.error(this.getClass().getCanonicalName()
								+ " failed to execute scheduled task : "
								+ crontab.commandClassName);
						e.printStackTrace();
					}
				} else {
					newList.add(crontab);
				}
			}
			crontabList = newList;
			for (Crontab crontab : crontabList) {
				try {
					Class.forName(crontab.commandClassName);
				} catch (ClassNotFoundException e) {
					throw new IllegalStateException(
							"Crontab file load error! (cannot load command class : "
									+ crontab.commandClassName + ")");
				}
				log.info("Crontab invocation : " + crontab.line);
			}
		} catch (IOException e) {
			log.error("Cannot read crontab.txt because of "
					+ e.getLocalizedMessage());
			e.printStackTrace();
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (Exception e2) {
				}
			}
			if (br != null) {
				try {
					br.close();
				} catch (Exception e2) {
				}
			}
			isInitialized = true;
			log.info("----------------------------------------");
		}
	}

	/**
	 * Add crontab configuration
	 * 
	 * @param line
	 *            a line in crontab file
	 */
	public void addCrontab(String line) {
		if (!line.trim().startsWith("#") && !line.trim().equals("")
				&& !isAlreadyRegistered(line)) {
			crontabList.add(CrontabUtil.read(line));
		}
	}

	/**
	 * Remove crontab configuration
	 * 
	 * @param line
	 *            a line in crontab file
	 */
	public void removeCrontab(String line) {
		if (isAlreadyRegistered(line)) {
			crontabList.remove(findByLine(line));
		}
	}

	/**
	 * Modify crontab configuration
	 * 
	 * @param before
	 *            the crontab line
	 * @param after
	 *            the crontab line
	 */
	public void editCronTab(String before, String after) {
		if (isAlreadyRegistered(before)) {
			removeCrontab(before);
		}
		addCrontab(after);
	}

	/**
	 * Is already registered crontab configuration or not
	 * 
	 * @param line
	 *            a line in crontab file
	 * @return result
	 */
	public boolean isAlreadyRegistered(String line) {
		for (Crontab crontab : crontabList) {
			if (crontab.line.equals(line)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Search the line in the already registered configurations
	 * 
	 * @param line
	 *            search target line
	 * @return result
	 */
	public Crontab findByLine(String line) {
		for (Crontab crontab : crontabList) {
			if (crontab.line.equals(line)) {
				return crontab;
			}
		}
		return null;
	}

	/**
	 * Logging cron daemon invocation if enabled.
	 * 
	 * @param message
	 *            log message
	 */
	protected void loggingAtEachInvocation(String message) {
		boolean isEnabled = containerAdaptor.getSchedulerConfig().enableLoggingForEachCrondInvocation;
		if (isEnabled) {
			log.info(message);
		}
	}

	/**
	 * Get list of scheduling text information
	 * 
	 * @return scheduling list
	 */
	public List<String> getSchedulingStringList() {
		String serverName = null;
		try {
			serverName = SchedulerUtil.getWorkingServerName(containerAdaptor
					.getSchedulerConfig(), SchedulerUtil.getHostname());
		} catch (Exception e) {
			e.printStackTrace();
		}
		List<String> schedulingList = new ArrayList<String>();
		for (Crontab crontab : crontabList) {
			if (CrontabUtil.isServerToInvoke(crontab, serverName)) {
				StringBuilder sb = new StringBuilder();
				sb.append(crontab.commandClassName);
				sb.append(",");
				if (crontab.isIntervalInvocation) {
					sb.append("interval:");
					sb.append(crontab.intervalSeconds);
					sb.append("sec");
				} else {
					sb.append("next:");
					sb.append(CalendarUtil.toYYYYMMDDHHMISS(CalendarUtil
							.getCalendar(crontab.nextInvocationTime)));
				}
				schedulingList.add(sb.toString());
			}
		}
		return schedulingList;
	}
}
